package com.haizhi.storage;

import com.haizhi.util.GenerateMd5;
import com.haizhi.util.Tools;
import org.apache.http.HttpHeaders;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.PosixFilePermission;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.stream.Stream;

@Service
public class StorageServiceImpl implements StorageService {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    private final Path upLocation;
    private final Path downLocation;

    // 存储md5 信息
    private ConcurrentMap<String, FilesInfo> storageInfoMap;

    @Autowired
    public StorageServiceImpl(Environment ev) {
        this.upLocation = Paths.get(ev.getProperty("upload.location"));
        this.downLocation = Paths.get(ev.getProperty("download.location"));
        storageInfoMap = new ConcurrentHashMap<>();

        //开启文件扫描线程..
        Thread scanFileMd5Thread = new Thread(() -> {
            while (true) {
                long startTime = System.currentTimeMillis();
                logger.info("开始扫描文件, 计算MD5..");
                loadAllDownLoadFileMd5();
                logger.info("扫描文件完成, 耗时: {}", System.currentTimeMillis() - startTime);
                try {
                    logger.info("开始休眠扫描线程...");
                    Thread.sleep(1000 * 60 * 10);
                    logger.info("扫描线程休眠结束...");
                } catch (InterruptedException e) {
                    logger.error("ERROR", e);
                }
            }
        });
        scanFileMd5Thread.setDaemon(true);
        scanFileMd5Thread.start();
    }

    private void loadAllDownLoadFileMd5() {
        loadAllDownload().forEach(filePath -> {

            String fileName = filePath.toString();
            Path fullPath = loadDownload(fileName);

            try {
                //获取最后修改时间
                String fileTime = Files.getLastModifiedTime(fullPath).toString();
                if (storageInfoMap.containsKey(fileName)) {


                    logger.info("file = {} lastTime = {}", fullPath, fileTime);

                    FilesInfo filesInfo = storageInfoMap.get(fileName);
                    if (Objects.equals(filesInfo.getLastTime(), fileTime)) {
                        return;
                    }

                    //如果最后修改时间修改了, 重新计算MD5
                    filesInfo.setMd5(GenerateMd5.getHash(fullPath));
                    filesInfo.setLastTime(fileTime);
                    return;
                }
                storageInfoMap.put(fileName, new FilesInfo(GenerateMd5.getHash(fullPath), fileName, fileTime));
            } catch (Exception e) {
                logger.error("ERROR", e);
            }
        });
    }

    @Override
    public boolean storeUpload(MultipartFile multipartFile, String relatePath, HttpServletRequest request) {
        try {
            if (multipartFile == null || relatePath == null || Objects.equals(relatePath, "")) {
                logger.error("文件句柄为NULL...");
                return false;
            }

            Path realPath = this.upLocation.resolve(relatePath);
            Tools.createDirs(realPath);

            String md5 = request.getHeader(HttpHeaders.CONTENT_MD5);

            if (multipartFile.isEmpty()) {
                throw new StorageFileEmptyException("Failed to storeUpload empty file " + relatePath);
            }

            Files.copy(multipartFile.getInputStream(), realPath, StandardCopyOption.REPLACE_EXISTING);

            // 如果有传入MD5则需要进行校验
            if (md5 != null && !Objects.equals(md5, "") && !Objects.equals(md5, "null")) {
                String local_md5 = GenerateMd5.getHash(realPath);
                if (!Objects.equals(md5, local_md5)) {
                    logger.error("file上传失败: {} remote md5 = {} local md5 = {}", relatePath, md5, local_md5);
                    return false;
                }
            }

            logger.info("文件{}上传成功!", relatePath);
        } catch (Exception e) {
            throw new StorageIOException("Failed to storeUpload file " + relatePath, e);
        }

        return true;
    }

    @Override
    public Stream<Path> loadAllDownload() {
        try {
            return Files.walk(this.downLocation)
                    .filter(path -> !path.equals(this.downLocation) && !Files.isDirectory(path))
                    .map(this.downLocation::relativize);
        } catch (IOException e) {
            throw new StorageException("Failed to read stored file", e);
        }
    }

    @Override
    public Stream<Path> loadAllUpload(int maxDepth) {
        try {
            if (maxDepth > 0) {
                return Files.walk(this.upLocation, maxDepth)
                        .filter(path -> !path.equals(this.upLocation))
                        .map(this.upLocation::relativize);
            } else {
                return Files.walk(this.upLocation)
                        .filter(path -> !path.equals(this.upLocation))
                        .map(this.upLocation::relativize);
            }
        } catch (IOException e) {
            throw new StorageException("Failed to read stored file", e);
        }
    }

    @Override
    public Path loadDownload(String filename) {
        return downLocation.resolve(filename);
    }

    @Override
    public void loadDownAsResource(String filename, HttpServletResponse response,
                                   HttpServletRequest request) {

        long pos = 0L;
        long contentLength;
        int rangeSwitch = 0; // 0,从头开始的全文下载；1,从某字节开始的下载（bytes=27000-）；2,从某字节开始到某字节结束的下载（bytes=27000-39000）
        long fileLength;

        int n_byte;
        long readLength = 0;
        int byte_size = 1024 * 8;
        byte[] bytes = new byte[byte_size];

        long start_pos = 0;
        long end_pos = 0;

        String rangBytes;
        BufferedInputStream bis = null;
        InputStream ins = null;
        OutputStream out = null;

        Path filepath = loadDownload(filename);
        logger.info("加载文件名称: filename = {}", filename);
        logger.info("加载文件路径: filepath = {}", filepath);

        try {
            File file = filepath.toFile();

            if (!file.exists()) {
                logger.error("文件不存在: file = {}", filename);
                return;
            }


            fileLength = file.length();

            // get file content
            ins = new FileInputStream(file);
            bis = new BufferedInputStream(ins);

            // tell the client to allow accept-ranges
            response.reset();
            response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes");

            // client requests a file block download start byte
            String range = request.getHeader(HttpHeaders.RANGE);
            if (range != null && range.trim().length() > 0 && !"null".equals(range)) {

                logger.info("断点续传: file = {} range = {}", filename, range);
                response.setStatus(javax.servlet.http.HttpServletResponse.SC_PARTIAL_CONTENT);
                rangBytes = range.replaceAll("bytes=", "");
                if (rangBytes.endsWith("-")) {  // bytes=270000-
                    rangeSwitch = 1;
                    pos = Long.parseLong(rangBytes.substring(0, rangBytes.indexOf("-")));

                    //防止超出长度
                    if (pos >= fileLength) {
                        pos = fileLength - 1;
                        contentLength = 1;
                    } else {
                        contentLength = fileLength - pos;  // 客户端请求的是270000之后的字节（包括bytes下标索引为270000的字节）
                    }
                } else { // bytes=270000-320000
                    rangeSwitch = 2;
                    String temp1 = rangBytes.substring(0, rangBytes.indexOf("-"));
                    String temp2 = rangBytes.substring(rangBytes.indexOf("-") + 1, rangBytes.length());

                    long tmp_start_pos = Long.parseLong(temp1);
                    long tmp_end_pos = Long.parseLong(temp2);
                    if (tmp_start_pos < 0) {
                        tmp_start_pos = 0;
                    }
                    if (tmp_end_pos < 0) {
                        tmp_end_pos = 0;
                    }
                    start_pos = Math.min(tmp_start_pos, tmp_end_pos);
                    end_pos = Math.max(tmp_start_pos, tmp_end_pos);

                    if (start_pos >= fileLength) {
                        pos = fileLength - 1;
                        contentLength = 1;
                    } else if (end_pos >= fileLength) {
                        pos = start_pos;
                        contentLength = fileLength - start_pos;
                    } else {
                        pos = start_pos;
                        contentLength = end_pos - start_pos + 1;
                    }
                }
            } else {
                contentLength = fileLength;
            }

            logger.info("文件传输起始位置: file = {} pos = {}", filename, pos);

            // 如果设设置了Content-Length，则客户端会自动进行多线程下载。如果不希望支持多线程，则不要设置这个参数。
            // Content-Length: [文件的总大小] - [客户端请求的下载的文件块的开始字节]
            response.setHeader("Content-Length", Long.toString(contentLength));

            if (Objects.equals(request.getHeader(HttpHeaders.CONNECTION), "Close")) {
                logger.info("短连接: file = {}", filename);
                return;
            }

            // 断点开始
            // 响应的格式是:
            // Content-Range: bytes [文件块的开始字节]-[文件的总大小 - 1]/[文件的总大小]
            if (rangeSwitch == 1) {
                String contentRange = "bytes " +
                        Long.toString(pos) + "-" +
                        Long.toString(fileLength - 1) + "/" +
                        Long.toString(fileLength);

                response.setHeader(HttpHeaders.CONTENT_RANGE, contentRange);
                bis.skip(pos);
            } else if (rangeSwitch == 2) {
                String contentRange = range.replace("=", " ") + "/" + Long.toString(fileLength);
                response.setHeader(HttpHeaders.CONTENT_RANGE, contentRange);
                bis.skip(pos);
            } else {
                String contentRange = "bytes " + "0-" + (fileLength - 1) + "/" + fileLength;
                response.setHeader(HttpHeaders.CONTENT_RANGE, contentRange);
            }

            //String fileName = file.getName();
            response.setContentType("application/octet-stream");
//            response.addHeader(HttpHeaders., "attachment;filename=" + fileName);

            out = response.getOutputStream();
            if (rangeSwitch == 2) {
                // 针对 bytes=27000-39000 的请求，从27000开始写数据
                while (readLength <= contentLength - byte_size) {
                    n_byte = bis.read(bytes);
                    readLength += n_byte;
                    out.write(bytes, 0, n_byte);
                }
                if (readLength <= contentLength) {
                    n_byte = bis.read(bytes, 0, (int) (contentLength - readLength));
                    out.write(bytes, 0, n_byte);
                }

            } else {
                while ((n_byte = bis.read(bytes)) != -1) {
                    out.write(bytes, 0, n_byte);
                }
            }
            logger.info("完成 rangeSwitch = {} 数据读写", rangeSwitch);
            out.flush();
            logger.info("强制刷新成功...");
        } catch (Exception e) {
            logger.error("ERROR", e);
        } finally {

            try {
                if (ins != null) {
                    ins.close();
                    logger.info("ins 资源释放成功..");
                }
            } catch (IOException e) {
                logger.error("ERROR", e);
            }
            try {
                if (out != null) {
                    out.close();
                    logger.info("out 资源释放成功..");
                }
            } catch (IOException e) {
                logger.error("ERROR", e);
            }
            try {
                if (bis != null) {
                    bis.close();
                    logger.info("bis 资源释放成功..");
                }
            } catch (IOException e) {
                logger.error("ERROR", e);
            }
        }
        logger.info("下载完成: file = {} start_pos = {} end_pos = {}", filename, start_pos, end_pos);
    }

    @Override
    public String getDownFileMD5(String fileName) {
        String md5 = "";
        try {

            Path filePath = loadDownload(fileName);
            logger.info("当前获取MD5的文件名称: filename = {}", fileName);
            logger.info("当前获取MD5的文件路径: filepath = {}", filePath);
            if (!Files.exists(filePath)) {
                logger.error("文件不存在: file = {}", fileName);
                return md5;
            }

            if (!storageInfoMap.containsKey(fileName)) {
                logger.warn("文件还未扫描到: {}", fileName);
                return md5;
            }

            //获取最后修改时间
            String fileTime = Files.getLastModifiedTime(filePath).toString();
            logger.info("file = {} lastTime = {}", filePath, fileTime);

            FilesInfo filesInfo = storageInfoMap.get(fileName);
            if (Objects.equals(filesInfo.getLastTime(), fileTime)) {
                return filesInfo.getMd5();
            }

            // 如果修改时间变了 则移除MD5记录
            storageInfoMap.remove(fileName);
        } catch (Exception e) {
            logger.error("ERROR", e);
        }

        return md5;
    }

    @Override
    public boolean md5InMap(Path filePath) {

        String fileName = filePath.toString();
        if (!storageInfoMap.containsKey(fileName)) {
            return false;
        }

        Path fullPath = loadDownload(fileName);
        try {
            String fileTime = Files.getLastModifiedTime(fullPath).toString();
            FilesInfo filesInfo = storageInfoMap.get(fileName);
            if (Objects.equals(filesInfo.getLastTime(), fileTime)) {
                return true;
            }

            // 如果修改时间变了 则移除MD5记录
            storageInfoMap.remove(fileName);
        } catch (IOException e) {
            logger.error("ERROR", e);
        }

        return false;
    }

    @Override
    public void storageInit() {
        try {
            //文件夹权限设置
            Set<PosixFilePermission> perms = new HashSet<>();
            perms.add(PosixFilePermission.OWNER_READ);
            perms.add(PosixFilePermission.OWNER_WRITE);
            perms.add(PosixFilePermission.OWNER_EXECUTE);
            perms.add(PosixFilePermission.GROUP_READ);
            perms.add(PosixFilePermission.GROUP_WRITE);
            perms.add(PosixFilePermission.GROUP_EXECUTE);
            perms.add(PosixFilePermission.OTHERS_WRITE);
            perms.add(PosixFilePermission.OTHERS_READ);
            perms.add(PosixFilePermission.OTHERS_EXECUTE);

            if (Files.notExists(upLocation)) {
                Files.createDirectories(upLocation);
            }
            if (Files.exists(upLocation)) {
                Files.setPosixFilePermissions(upLocation, perms);
            } else {
                throw new IOException(upLocation + "文件夹不存在...");
            }

            if (Files.notExists(downLocation)) {
                Files.createDirectories(downLocation);
            }
            if (Files.exists(downLocation)) {
                Files.setPosixFilePermissions(downLocation, perms);
            } else {
                throw new IOException(downLocation + "文件夹不存在...");
            }
            logger.info("上传路径: upLocation = {}", upLocation.toAbsolutePath());
            logger.info("下载路径: downLocation = {}", downLocation.toAbsolutePath());
            logger.info("目录初始化完成...");
        } catch (IOException e) {
            throw new StorageException("Could not initialize storage", e);
        }
    }
}
